Микропроцессор может выполнять целочисленные операции и операции с плавающей точкой. Для этого в его архитектуре есть два отдельных блока:
Рис. 1. Классификация арифметических команд
Группа арифметических целочисленных команд работает с двумя типами чисел:
Размерность целого двоичного числа может составлять 8, 16 или 32 бит. Знак двоичного числа определяется тем, как интерпретируется старший бит в представлении числа. Это 7-й, 15-й или 31-й биты для чисел соответствующей размерности (см. Типы данных ). При этом интересно то, что среди арифметических команд есть всего две команды, которые действительно учитывают этот старший разряд как знаковый, — это команды целочисленного умножения и деления imul и idiv. В остальных случаях ответственность за действия со знаковыми числами и, соответственно, со знаковым разрядом ложится на программиста. К этому вопросу мы вернемся чуть позже. Диапазон значений двоичного числа зависит от его размера и трактовки старшего бита либо как старшего значащего бита числа, либо как бита знака числа (табл. 1).
Таблица 1. Диапазон значений двоичных чисел
| Размерность поля | Целое без знака | Целое со знаком |
| байт | 0...255 | –128...+127 |
| слово | 0...65 535 | –32 768...+32 767 |
| двойное слово | 0...4 294 967 295 | –2 147 483 648...+2 147 483 647 |
Это делается с использованием директив описания данных. К примеру, последовательность описаний двоичных чисел из сегмента данных листинга 1 (помните о принципе “младший байт по младшему адресу”) будет выглядеть в памяти так, как показано на рис. 2.
Листинг 1. Числа с фиксированной точкой ;prg_8_1.asm masm model small stack 256 .data ;сегмент данных per_1 db 23 per_2 dw 9856 per_3 dd 9875645 per_4 dw 29857 .code ;сегмент кода main: ;точка входа в программу mov ax,@data ;связываем регистр dx с сегментом mov ds,ax ;данных через регистр ax exit: ;посмотрите в отладчике дамп сегмента данных mov ax,4c00h ;стандартный выход int 21h end main ;конец программы |
Рис. 2. Дамп
памяти для сегмента данных листинга 1
Рис. 3. Представление BCD-чисел
Как описать двоично-десятичные числа в программе?
Для этого можно использовать только две директивы описания и инициализации данных — db и dt. Возможность применения только этих директив для описания BCD-чисел обусловлена тем, что к таким числам также применим принцип “младший байт по младшему адресу”, что, как мы увидим далее, очень удобно для их обработки. И вообще, при использовании такого типа данных как BCD-числа, порядок описания этих чисел в программе и алгоритм их обработки — это дело вкуса и личных пристрастий программиста. Это станет ясно после того, как мы ниже рассмотрим основы работы с BCD-числами. К примеру, приведенная в сегменте данных листинга 2 последовательность описаний BCD-чисел будет выглядеть в памяти так, как показано на рис. 4.
Листинг 2. BCD-числа ;prg_8_2.asm masm model small stack 256 .data ;сегмент данных per_1 db 2,3,4,6,8,2 ;неупакованное BCD-число 286432 per_3 dt 9875645 ;упакованное BCD-число 9875645 .code ;сегмент кода main: Листинг 2. BCD-числа ;prg_8_2.asm masm model small stack 256 .data ;сегмент данных per_1 db 2,3,4,6,8,2 ;неупакованное BCD-число 286432 ax ;данных через регистр ax exit: ;поссегмент кода main: Листинг 2. BCD-числа ;prg_8_2.asm masm model small stack 256 .data ;сегмент данных per_1 db 2,3,4,6,8,2 h end main ;конец программы |
Рис. 4. Дамп памяти для сегмента данных листинга 2
После столь подробного обсуждения объектов, с которыми работают арифметические операции, можно приступить к рассмотрению средств их обработки на уровне системы команд микропроцессора.
операнд_1 = операнд_1 + операнд_2 + значение_cf
Рассмотрим пример вычисления суммы чисел (листинг 3).
Листинг 3. Вычисление суммы чисел <1> ;prg_8_3.asm <2> masm <3> model small <4> stack 256 <5> .data <6> a db 254 <7> .code ;сегмент кода <8> main: <9> mov ax,@data <10> mov ds,ax <11> ... <12> xor ax,ax <13> add al,17 <14> add al,a <15> jnc m1 ;если нет переноса, то перейти на m1 <16> adc ah,0 ;в ax сумма с учетом переноса <17> m1: ... <18> exit: <19> mov ax,4c00h ;стандартный выход <20> int 21h <21> end main ;конец программы |
Вы, конечно, помните, как представляются числа в компьютере: положительные — в двоичном коде, отрицательные — в дополнительном коде. Рассмотрим различные варианты сложения чисел. Примеры призваны показать поведение двух старших битов операндов и правильность результата операции сложения.
30566 = 01110111 01100110 + 00687 = 00000010 10101111 = 31253 = 01111010 00010101Следим за переносами из 14-го и 15-го разрядов и правильностью результата: переносов нет, результат правильный.
30566 = 01110111 01100110 + 30566 = 01110111 01100110 = 61132 = 11101110 11001100Произошел перенос из 14-го разряда; из 15-го разряда переноса нет. Результат неправильный, так как имеется переполнение — значение числа получилось больше, чем то, которое может иметь 16-битное число со знаком (+32 767).
-30566 = 10001000 10011010 + -04875 = 11101100 11110101 = -35441 = 01110101 10001111Произошел перенос из 15-го разряда, из 14-го разряда нет переноса. Результат неправильный, так как вместо отрицательного числа получилось положительное (в старшем бите находится 0).
-4875 = 11101100 11110101 + -4875 = 11101100 11110101 = -9750 = 11011001 11101010Есть переносы из 14 и 15-го разрядов. Результат правильный.
Таким образом, мы исследовали все случаи и выяснили, что ситуация переполнения (установка флага of в 1) происходит при переносе:
Итак, переполнение регистрируется с помощью флага переполнения of. Дополнительно к флагу of при переносе из старшего разряда устанавливается в 1 и флаг переноса cf. Так как микропроцессор не знает о существовании чисел со знаком и без знака, то вся ответственность за правильность действий с получившимися числами ложится на программиста. Проанализировать флаги cf и of можно командами условного перехода jc\jnc и jo\jno соответственно.
Что же касается команд сложения чисел со знаком, то они те же, что и для чисел без знака.
05 = 00000000 00000101 -10 = 00000000 00001010 Для того чтобы произвести вычитание, произведем воображаемый заем из старшего разряда: 100000000 00000101 - 00000000 00001010 = 11111111 11111011Тем самым по сути выполняется действие
(65 536 + 5) — 10 = 65 531,
0 здесь как бы эквивалентен числу 65 536. Результат, конечно, неверен, но микропроцессор считает, что все нормально, хотя факт заема единицы он фиксирует установкой флага переноса cf. Но посмотрите еще раз внимательно на результат операции вычитания. Это же –5 в дополнительном коде! Проведем эксперимент: представим разность в виде суммы 5 + (–10).
5 = 00000000 00000101 + (-10)= 11111111 11110110 = 1111111111111011то есть мы получили тот же результат, что и в предыдущем примере.
Таким образом, после команды вычитания чисел без знака нужно анализировать состояние флага cf. Если он установлен в 1, то это говорит о том, что произошел заем из старшего разряда и результат получился в дополнительном коде.
Аналогично командам сложения, группа команд вычитания состоит из минимально возможного набора. Эти команды выполняют вычитание по алгоритмам, которые мы сейчас рассматриваем, а учет особых ситуаций должен производиться самим программистом. К командам вычитания относятся следующие:
Рассмотрим пример (листинг 4) программной обработки ситуации, разобранной в примере 6.
Листинг 4. Проверка при вычитании чисел без знака <1> ;prg_8_4.asm <2> masm <3> model small <4> stack 256 <5> .data <6> .code ;сегмент кода <7> main: ;точка входа в программу <8> ... <9> xor ax,ax <10> mov al,5 <11> sub al,10 <12> jnc m1 ;нет переноса? <13> neg al ;в al модуль результата <14> m1: ... <15> exit: <16> mov ax,4c00h ;стандартный выход <17> int 21h <18> end main ;конец программы |
45 = 0010 1101 - -127 = 1000 0001 = -44 = 1010 1100Судя по знаковому разряду, результат получился отрицательный, что, в свою очередь, говорит о том, что число нужно рассматривать как дополнение, равное –44. Правильный результат должен быть равен 172. Здесь мы, как и в случае знакового сложения, встретились с переполнением мантиссы, когда значащий разряд числа изменил знаковый разряд операнда. Отследить такую ситуацию можно по содержимому флага переполнения of. Его установка в 1 говорит о том, что результат вышел за диапазон представления знаковых чисел (то есть изменился старший бит) для операнда данного размера, и программист должен предусмотреть действия по корректировке результата.
Другой пример разности рассматривается в примере 7, но выполним мы ее способом сложения.
-45 — 45 = -45 + (-45)= -90. -45 = 1101 0011 + -45 = 1101 0011 = -90 = 1010 0110Здесь все нормально, флаг переполнения of сброшен в 0, а 1 в знаковом разряде говорит о том, что значение результата — число в дополнительном коде.
На рис. 5 по шагам показана технология сложения длинных чисел. Видно, что процесс сложения многобайтных чисел происходит так же, как и при сложении двух чисел “в столбик”, — с осуществлением, при необходимости, переноса 1 в старший разряд. Если нам удастся запрограммировать этот процесс, то мы значительно расширим диапазон двоичных чисел, над которыми мы сможем выполнять операции сложения и вычитания.
Принцип вычитания чисел с диапазоном представления, превышающим стандартные разрядные сетки операндов, тот же, что и при сложении, то есть используется флаг переноса cf. Нужно только представлять себе процесс вычитания в столбик и правильно комбинировать команды микропроцессора с командой sbb.
В завершение обсуждения команд сложения и вычитания отметим, что кроме флагов cf и of в регистре eflags есть еще несколько флагов, которые можно использовать с двоичными арифметическими командами. Речь идет о следующих флагах:
mul сомножитель_1
Как видите, в команде указан всего лишь один операнд-сомножитель. Второй операнд — сомножитель_2 задан неявно. Его местоположение фиксировано и зависит от размера сомножителей. Так как в общем случае результат умножения больше, чем любой из его сомножителей, то его размер и местоположение должны быть тоже определены однозначно. Варианты размеров сомножителей и размещения второго операнда и результата приведены в табл. 2.
Таблица 2. Расположение операндов и результата при умножении
| сомножитель_1 | сомножитель_2 | Результат |
| Байт | al | 16 бит в ax:
al — младшая часть результата; ah — старшая часть результата |
| Слово | ax | 32 бит в паре dx:ax:
ax — младшая часть результата; dx — старшая часть результата |
| Двойное слово | eax | 64 бит в паре edx:eax:
eax — младшая часть результата; edx — старшая часть результата |
Из таблицы видно, что произведение состоит из двух частей и в зависимости от размера операндов размещается в двух местах — на месте сомножитель_2 (младшая часть) и в дополнительном регистре ah, dx, edx (старшая часть). Как же динамически (то есть во время выполнения программы) узнать, что результат достаточно мал и уместился в одном регистре или что он превысил размерность регистра и старшая часть оказалась в другом регистре? Для этого привлекаются уже известные нам по предыдущему обсуждению флаги переноса cf и переполнения of:
Листинг 5. Умножение <1> ;prg_8_5.asm <2> masm <3> model small <4> stack 256 <5> .data ;сегмент данных <6> rez label word <7> rez_l db 45 <8> rez_h db 0 <9> .code ;сегмент кода <10> main: ;точка входа в программу <11> ... <12> xor ax,ax <13> mov al,25 <14> mul rez_l <15> jnc m1 ;если переполнение, то на м1 <16> mov rez_h,ah ;старшую часть результата в rez_h <17> m1: <18> mov rez_l,al <19> exit: <20> mov ax,4c00h ;стандартный выход <21> int 21h <22> end main ;конец программы |
imul операнд_1[,операнд_2,операнд_3]
Эта команда выполняется так же, как и команда mul. Отличительной
особенностью команды imul является только формирование знака.
Если результат мал и умещается в одном регистре (то есть
если cf = of = 0), то содержимое другого регистра (старшей части) является
расширением знака — все его биты равны старшему биту (знаковому разряду)
младшей части результата.
В противном случае (если cf = of = 1) знаком результата
является знаковый бит старшей части результата, а знаковый бит младшей
части является значащим битом двоичного кода результата.
Если вы посмотрите описание команды imul, то увидите, что она допускает более широкие возможности по заданию местоположения операндов. Это сделано для удобства использования.
div делитель
Делитель может находиться в памяти или в регистре и иметь размер 8, 16 или 32 бит. Местонахождение делимого фиксировано и так же, как в команде умножения, зависит от размера операндов. Результатом команды деления являются значения частного и остатка.
Варианты местоположения и размеров операндов операции деления показаны в табл. 3.
Таблица 3. Расположение операндов и результата при делении
| Делимое | Делитель | Частное | Остаток |
| 16 бит
в регистре ax |
Байт
регистр или ячейка памяти |
Байт
в регистре al |
Байт
в регистре ah |
| 32 бит
dx — старшая часть ax — младшая часть |
Слово 16 бит
регистр или ячейка памяти |
Слово 16 бит в
регистре ax |
Слово 16 бит в
регистре dx |
| 64 бит
edx — старшая часть eax — младшая часть |
Двойное слово 32 бит
регистр или ячейка памяти |
Двойное слово
32 бит в регистре eax |
Двойное слово
32 бит в регистре edx |
После выполнения команды деления содержимое флагов неопределенно, но возможно возникновение прерывания с номером 0, называемого “деление на ноль”. Этот вид прерывания относится к так называемым исключениям. Эта разновидность прерываний возникает внутри микропроцессора из-за некоторых аномалий во время вычислительного процесса. Прерывание 0, “деление на ноль”, при выполнении команды div может возникнуть по одной из следующих причин:
Листинг 6. Деление чисел <1> ;prg_8.6.asm <2> masm <3> model small <4> stack 256 <5> .data <6> del_b label byte <7> deldw 29876 <8> delt db 45 <9> .code ;сегмент кода <10> main: ;точка входа в программу <11> ... <12> xor ax,ax <13> ;последующие две команды можно заменить одной mov ax,del <14> mov ah,del_b ;старший байт делимого в ah <15> mov al,del_b+1 ;младший байт делимого в al <16> div delt ;в al — частное, в ah — остаток <17> ... <18> endmain ;конец программы |
idiv делитель
Для этой команды справедливы все рассмотренные положения, касающиеся команд и чисел со знаком. Отметим лишь особенности возникновения исключения 0, “деление на ноль”, в случае чисел со знаком. Оно возникает при выполнении команды idiv по одной из следующих причин:
Существуют два вида команд преобразования типа:
К примеру, вычислим значение y = (a + b)/c, где a, b, c — байтовые знаковые переменные (листинг 7).
Листинг 7. Вычисление простого выражения <1> ;prg_8_9.asm <2> masm <3> model small <4> stack 256 <5> .data <6> a db ? <7> b db ? <8> c db ? <9> y dw 0 <10> .code <11> main: ;точка входа в программу <12> ... <13> xor ax,ax <14> mov al,a <15> cbw <16> movsx bx,b <17> add ax,bx <18> idiv c ;в al — частное, в ah — остаток <19> exit: ную команду удобно использовать для подготовки операндов без знака к выполнению арифметических действий. |